<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>音叉振动波形采集工具</title>
  <!-- 引入外部资源 -->
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#10B981',
            secondary: '#3B82F6',
            dark: '#0F172A',
          },
        },
      }
    }
  </script>
  
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .grid-bg {
        background-image: linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px),
                          linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px);
        background-size: 20px 20px;
      }
    }
  </style>
</head>
<body class="bg-dark text-white min-h-screen font-sans">
  <header class="bg-slate-900/80 backdrop-blur-md p-4 shadow-lg border-b border-slate-800">
    <div class="container mx-auto">
      <h1 class="text-2xl font-bold flex items-center gap-2">
        <i class="fa fa-tuning-fork text-primary"></i>
        音叉振动波形采集工具
      </h1>
    </div>
  </header>

  <main class="container mx-auto p-4 md:p-6">
    <!-- 教材原理说明 -->
    <div class="bg-slate-800/50 rounded-xl p-6 mb-8 border border-slate-700">
      <h2 class="text-xl font-bold mb-4">实验原理</h2>
      <p class="text-slate-300 mb-4">
        敲击音叉使其发声，振动的音叉带动周围空气振动，传感器将空气振动转化为电信号，
        计算机将电信号转化为图像。图像的横轴表示时间，纵轴反映振动幅度的变化。
      </p>
    </div>
    
    <!-- 控制面板 -->
    <div class="bg-slate-800/50 rounded-xl p-6 mb-8 border border-slate-700 shadow-lg">
      <h2 class="text-xl font-bold mb-6">采集控制</h2>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div class="flex items-center gap-4">
          <button id="sound1-btn" class="bg-primary hover:bg-primary/90 text-white font-medium py-3 px-8 rounded-full shadow-lg hover:shadow-primary/30 transition-all duration-300 flex items-center gap-2 transform hover:scale-105 active:scale-95">
            <i class="fa fa-play"></i>
            <span>开始采集 1</span>
          </button>
          <div class="flex items-center gap-2">
            <span class="w-4 h-4 rounded-full bg-gray-600" id="sound1-indicator"></span>
            <span class="text-slate-300" id="sound1-status">未采集</span>
          </div>
        </div>
        
        <div class="flex items-center gap-4">
          <button id="sound2-btn" class="bg-secondary hover:bg-secondary/90 text-white font-medium py-3 px-8 rounded-full shadow-lg hover:shadow-secondary/30 transition-all duration-300 flex items-center gap-2 transform hover:scale-105 active:scale-95">
            <i class="fa fa-play"></i>
            <span>开始采集 2</span>
          </button>
          <div class="flex items-center gap-2">
            <span class="w-4 h-4 rounded-full bg-gray-600" id="sound2-indicator"></span>
            <span class="text-slate-300" id="sound2-status">未采集</span>
          </div>
        </div>
      </div>
      
      <!-- 波形增强控制 -->
      <div class="mt-6 p-4 bg-slate-900/50 rounded-lg border border-slate-700">
        <h3 class="text-sm font-medium text-slate-300 mb-3">波形参数调整</h3>
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
          <div>
            <label class="text-xs text-slate-400 block mb-1">振动幅度放大</label>
            <input type="range" id="amplitude-gain" min="1" max="10" value="5" 
                  class="w-full accent-primary">
            <div class="flex justify-between text-xs text-slate-500 mt-1">
              <span>小</span>
              <span>中</span>
              <span>大</span>
            </div>
          </div>
          <div>
            <label class="text-xs text-slate-400 block mb-1">波形密度</label>
            <input type="range" id="density" min="32" max="256" step="32" value="64" 
                  class="w-full accent-primary">
            <div class="flex justify-between text-xs text-slate-500 mt-1">
              <span>稀疏</span>
              <span>密集</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 振动图像显示区域 -->
    <div class="space-y-8">
      <!-- 声音1振动图像 -->
      <div class="bg-slate-800/50 rounded-xl p-6 border border-slate-700">
        <div class="flex justify-between items-center mb-4">
          <h2 class="text-xl font-bold flex items-center gap-2">
            <i class="fa fa-area-chart text-primary"></i>
            振动波形 1
          </h2>
        </div>
        
        <div class="h-80 bg-slate-900 grid-bg rounded-lg border border-slate-700 overflow-hidden relative">
          <!-- 时间轴和振幅轴标记 -->
          <div class="absolute bottom-3 left-3 text-xs text-slate-400">
            时间（从左到右）
          </div>
          <div class="absolute top-1/2 left-3 -translate-y-1/2 text-xs text-slate-400 transform -rotate-90 origin-bottom-left">
            振动幅度
          </div>
          
          <canvas id="sound1-canvas" class="w-full h-full"></canvas>
          <div id="sound1-placeholder" class="absolute inset-0 flex flex-col items-center justify-center">
            <div class="flex gap-3 mb-4">
              <div class="w-4 h-32 bg-primary/60 rounded-full animate-pulse"></div>
              <div class="w-4 h-40 bg-primary/60 rounded-full animate-pulse" style="animation-delay: 100ms"></div>
              <div class="w-4 h-24 bg-primary/60 rounded-full animate-pulse" style="animation-delay: 200ms"></div>
              <div class="w-4 h-36 bg-primary/60 rounded-full animate-pulse" style="animation-delay: 300ms"></div>
              <div class="w-4 h-28 bg-primary/60 rounded-full animate-pulse" style="animation-delay: 400ms"></div>
            </div>
            <p class="text-slate-400">点击"开始采集 1"，然后敲击音叉</p>
          </div>
        </div>
      </div>
      
      <!-- 声音2振动图像 -->
      <div class="bg-slate-800/50 rounded-xl p-6 border border-slate-700">
        <div class="flex justify-between items-center mb-4">
          <h2 class="text-xl font-bold flex items-center gap-2">
            <i class="fa fa-area-chart text-secondary"></i>
            振动波形 2
          </h2>
        </div>
        
        <div class="h-80 bg-slate-900 grid-bg rounded-lg border border-slate-700 overflow-hidden relative">
          <!-- 时间轴和振幅轴标记 -->
          <div class="absolute bottom-3 left-3 text-xs text-slate-400">
            时间（从左到右）
          </div>
          <div class="absolute top-1/2 left-3 -translate-y-1/2 text-xs text-slate-400 transform -rotate-90 origin-bottom-left">
            振动幅度
          </div>
          
          <canvas id="sound2-canvas" class="w-full h-full"></canvas>
          <div id="sound2-placeholder" class="absolute inset-0 flex flex-col items-center justify-center">
            <div class="flex gap-3 mb-4">
              <div class="w-4 h-32 bg-secondary/60 rounded-full animate-pulse"></div>
              <div class="w-4 h-40 bg-secondary/60 rounded-full animate-pulse" style="animation-delay: 100ms"></div>
              <div class="w-4 h-24 bg-secondary/60 rounded-full animate-pulse" style="animation-delay: 200ms"></div>
              <div class="w-4 h-36 bg-secondary/60 rounded-full animate-pulse" style="animation-delay: 300ms"></div>
              <div class="w-4 h-28 bg-secondary/60 rounded-full animate-pulse" style="animation-delay: 400ms"></div>
            </div>
            <p class="text-slate-400">点击"开始采集 2"，然后敲击音叉</p>
          </div>
        </div>
      </div>
    </div>
    
    <div class="mt-8 p-6 bg-slate-800/50 rounded-xl border border-slate-700">
      <h3 class="text-lg font-bold mb-3">实验说明</h3>
      <ul class="text-slate-300 space-y-2 text-sm">
        <li>• 支持两个独立通道的振动波形采集，可分别操作</li>
        <li>• 每次点击"开始采集"都会重新初始化采集过程，支持多次采集</li>
        <li>• 敲击音叉后立即靠近麦克风，可获得最清晰的波形</li>
        <li>• 停止采集后，将保留当前实时显示的波形</li>
        <li>• 可通过"波形密度"调整显示效果，使其更接近教材样式</li>
      </ul>
    </div>
  </main>

  <footer class="mt-12 bg-slate-900/80 backdrop-blur-md p-4 text-center text-slate-400 text-sm border-t border-slate-800">
    音叉振动波形采集工具 © 2023
  </footer>

  <script>
    // 全局设置
    let amplitudeGain = 5;      // 振幅放大倍数
    let densityValue = 64;      // 波形点密度

    // 声音采集类
    class SoundCapture {
      constructor(id, color) {
        // 元素引用
        this.id = id;
        this.color = color;
        this.canvas = document.getElementById(`${id}-canvas`);
        this.ctx = this.canvas.getContext('2d');
        this.placeholder = document.getElementById(`${id}-placeholder`);
        this.btn = document.getElementById(`${id}-btn`);
        this.indicator = document.getElementById(`${id}-indicator`);
        this.statusText = document.getElementById(`${id}-status`);
        
        // 波形参数
        this.isActive = false;
        this.audioContext = null;
        this.analyser = null;
        this.microphone = null;
        this.stream = null;
        this.animationId = null;
        this.bufferLength = densityValue;
        this.dataArray = new Uint8Array(this.bufferLength);
        this.wavePoints = [];
        this.retainedWave = [];
        this.currentX = 0;
        this.scrollSpeed = 1.5;
        this.amplitudeBoost = amplitudeGain;
        
        // 视觉效果参数
        this.lineWidth = 1.5;     // 正常线条粗细
        this.fillOpacity = 0.1;
        this.shadowBlur = 0;
        
        // 初始化
        this.initCanvas();
        this.bindEvents();
      }
      
      // 初始化画布
      initCanvas() {
        const resize = () => {
          const dpr = window.devicePixelRatio || 1;
          const rect = this.canvas.getBoundingClientRect();
          this.canvas.width = rect.width * dpr;
          this.canvas.height = rect.height * dpr;
          this.ctx.scale(dpr, dpr);
          this.canvas.style.width = `${rect.width}px`;
          this.canvas.style.height = `${rect.height}px`;
          this.canvasWidth = rect.width;
          
          if (this.retainedWave.length > 0) {
            this.drawRetainedWave();
          }
        };
        
        resize();
        window.addEventListener('resize', resize);
      }
      
      // 绑定事件
      bindEvents() {
        // 确保按钮点击事件正确绑定
        this.btn.addEventListener('click', () => this.toggleCapture());
      }
      
      // 更新振幅增益
      updateAmplitudeGain(value) {
        this.amplitudeBoost = value;
      }
      
      // 更新波形密度
      updateDensity(value) {
        this.bufferLength = value;
        this.dataArray = new Uint8Array(this.bufferLength);
        if (this.analyser) {
          this.analyser.fftSize = this.bufferLength * 2;
        }
      }
      
      // 切换采集状态
      toggleCapture() {
        if (this.isActive) {
          this.stopCapture();
        } else {
          this.startCapture();
        }
      }
      
      // 开始采集
      async startCapture() {
        try {
          this.cleanupPreviousCapture();
          
          // 重置参数
          this.wavePoints = [];
          this.retainedWave = [];
          this.currentX = 0;
          this.clearCanvas();
          
          // 创建音频上下文
          this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
          
          // 创建分析器节点
          this.analyser = this.audioContext.createAnalyser();
          this.analyser.fftSize = this.bufferLength * 2;
          this.analyser.smoothingTimeConstant = 0.3;
          
          // 获取麦克风权限
          this.stream = await navigator.mediaDevices.getUserMedia({ 
            audio: {
              autoGainControl: true,
              noiseSuppression: false
            }, 
            video: false 
          });
          
          // 创建媒体源
          this.microphone = this.audioContext.createMediaStreamSource(this.stream);
          this.microphone.connect(this.analyser);
          
          // 隐藏占位符
          this.placeholder.classList.add('hidden');
          
          // 更新UI
          this.isActive = true;
          this.btn.innerHTML = `<i class="fa fa-stop"></i><span>停止采集 ${this.id === 'sound1' ? '1' : '2'}</span>`;
          this.indicator.className = `w-4 h-4 rounded-full ${this.color === 'primary' ? 'bg-primary' : 'bg-secondary'} animate-pulse`;
          this.statusText.textContent = '正在采集...';
          
          // 开始动画
          this.animate();
          
        } catch (error) {
          console.error('采集失败:', error);
          this.statusText.textContent = '请允许麦克风权限';
          this.statusText.classList.add('text-red-400');
          this.cleanupPreviousCapture();
          this.resetUI();
        }
      }
      
      // 停止采集
      stopCapture() {
        // 保存当前波形
        this.retainedWave = [...this.wavePoints];
        this.drawRetainedWave();
        
        // 清理资源
        this.cleanupPreviousCapture();
        
        // 更新UI
        this.isActive = false;
        this.btn.innerHTML = `<i class="fa fa-play"></i><span>开始采集 ${this.id === 'sound1' ? '1' : '2'}</span>`;
        this.indicator.className = 'w-4 h-4 rounded-full bg-gray-600';
        this.statusText.textContent = '采集完成（已保留波形）';
      }
      
      // 清理资源
      cleanupPreviousCapture() {
        if (this.animationId) {
          cancelAnimationFrame(this.animationId);
          this.animationId = null;
        }
        
        if (this.stream) {
          this.stream.getTracks().forEach(track => track.stop());
          this.stream = null;
        }
        
        if (this.audioContext) {
          this.audioContext.close().then(() => {
            this.audioContext = null;
            this.analyser = null;
            this.microphone = null;
          });
        }
      }
      
      // 重置UI
      resetUI() {
        this.isActive = false;
        this.btn.innerHTML = `<i class="fa fa-play"></i><span>开始采集 ${this.id === 'sound1' ? '1' : '2'}</span>`;
        this.indicator.className = 'w-4 h-4 rounded-full bg-gray-600';
      }
      
      // 清空画布
      clearCanvas() {
        const rect = this.canvas.getBoundingClientRect();
        this.ctx.clearRect(0, 0, rect.width, rect.height);
      }
      
      // 动画循环
      animate() {
        this.animationId = requestAnimationFrame(() => this.animate());
        this.analyser.getByteTimeDomainData(this.dataArray);
        this.processData();
        this.drawLiveWave();
      }
      
      // 处理音频数据
      processData() {
        const rect = this.canvas.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;
        const centerY = height / 2;
        const xStep = this.scrollSpeed / this.bufferLength;
        const boost = this.amplitudeBoost;
        
        // 添加新的波形点
        for (let i = 0; i < this.bufferLength; i++) {
          this.currentX += xStep;
          
          // 循环显示
          if (this.currentX > width) {
            this.wavePoints = [];
            this.currentX = 0;
          }
          
          // 计算Y坐标
          let value = (this.dataArray[i] / 128.0) - 1;
          value = value > 0 ? Math.pow(value, 0.8) : -Math.pow(-value, 0.8);
          
          const y = value * (centerY * 0.7) * boost + centerY;
          this.wavePoints.push({x: this.currentX, y});
        }
      }
      
      // 绘制实时波形
      drawLiveWave() {
        const rect = this.canvas.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;
        const centerY = height / 2;
        
        this.clearCanvas();
        this.drawAxes(width, height, centerY);
        
        if (this.wavePoints.length < 2) return;
        
        // 绘制波形
        this.ctx.beginPath();
        this.ctx.moveTo(this.wavePoints[0].x, this.wavePoints[0].y);
        
        for (let i = 1; i < this.wavePoints.length; i++) {
          this.ctx.lineTo(this.wavePoints[i].x, this.wavePoints[i].y);
        }
        
        // 设置线条样式
        const strokeColor = this.color === 'primary' ? 
          'rgba(16, 185, 129, 0.95)' : 
          'rgba(59, 130, 246, 0.95)';
        this.ctx.strokeStyle = strokeColor;
        this.ctx.lineWidth = this.lineWidth;
        this.ctx.lineCap = 'round';
        this.ctx.lineJoin = 'round';
        this.ctx.stroke();
      }
      
      // 绘制保留的波形
      drawRetainedWave() {
        if (!this.retainedWave || this.retainedWave.length < 2) return;
        
        const rect = this.canvas.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;
        const centerY = height / 2;
        
        this.clearCanvas();
        this.drawAxes(width, height, centerY);
        
        // 绘制波形
        this.ctx.beginPath();
        this.ctx.moveTo(this.retainedWave[0].x, this.retainedWave[0].y);
        
        for (let i = 1; i < this.retainedWave.length; i++) {
          this.ctx.lineTo(this.retainedWave[i].x, this.retainedWave[i].y);
        }
        
        // 设置线条样式
        const strokeColor = this.color === 'primary' ? 
          'rgba(16, 185, 129, 0.95)' : 
          'rgba(59, 130, 246, 0.95)';
        this.ctx.strokeStyle = strokeColor;
        this.ctx.lineWidth = this.lineWidth;
        this.ctx.lineCap = 'round';
        this.ctx.lineJoin = 'round';
        this.ctx.stroke();
      }
      
      // 绘制坐标轴
      drawAxes(width, height, centerY) {
        this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
        this.ctx.lineWidth = 1;
        
        // 横轴（时间轴）
        this.ctx.beginPath();
        this.ctx.moveTo(0, height - 10);
        this.ctx.lineTo(width, height - 10);
        this.ctx.stroke();
        
        // 纵轴（振动幅度轴）
        this.ctx.beginPath();
        this.ctx.moveTo(10, 0);
        this.ctx.lineTo(10, height);
        this.ctx.stroke();
      }
    }
    
    // 页面加载完成后初始化
    document.addEventListener('DOMContentLoaded', () => {
      // 初始化两个采集通道
      window.sound1 = new SoundCapture('sound1', 'primary');
      window.sound2 = new SoundCapture('sound2', 'secondary');
      
      // 绑定滑块事件
      document.getElementById('amplitude-gain').addEventListener('input', (e) => {
        amplitudeGain = parseFloat(e.target.value);
        if (window.sound1) window.sound1.updateAmplitudeGain(amplitudeGain);
        if (window.sound2) window.sound2.updateAmplitudeGain(amplitudeGain);
      });

      document.getElementById('density').addEventListener('input', (e) => {
        densityValue = parseInt(e.target.value);
        if (window.sound1) window.sound1.updateDensity(densityValue);
        if (window.sound2) window.sound2.updateDensity(densityValue);
      });
    });
  </script>
</body>
</html>